/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package com.velocityme.session;

import com.velocityme.entity.EncodedLicence;
import com.velocityme.entity.Login;
import com.velocityme.entity.User;
import com.velocityme.enums.LoginStatus;
import com.velocityme.utility.Licence;
import com.velocityme.utility.ServerConfiguration;
import com.velocityme.utility.SignedLicenceKey;
import java.beans.XMLDecoder;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.PublicKey;
import java.security.Signature;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.zip.DataFormatException;
import java.util.zip.Inflater;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.NoResultException;
import javax.persistence.PersistenceContext;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import sun.misc.BASE64Decoder;

/**
 *
 * @author rcrida
 */
@Stateless
public class LicenceKeyBean implements LicenceKeyLocal {

    @PersistenceContext
    private EntityManager em;

    private final Log log = LogFactory.getLog(LicenceKeyBean.class);


    //private static final long LOGOUT_DELAY = 10 * 60 * 1000; // 10 minutes
    private static final long LOGOUT_DELAY = 60 * 60 * 1000; // 1 hour
    
    /**
     * Try to assign a licence for the specified user. This checks the licence key
     * to see if it is valid and can be applied to the user.
     *
     * @param user the user to assign a licence to
     * @param remoteHost the host name that the user has logged in from
     * @return new login or null if invalid
     **/
    public Login assignLicense(final User user, final String remoteHost) {
        for (Object object: em.createNamedQuery("findAllLicenceKeys").getResultList()) {
            EncodedLicence licenceKey = (EncodedLicence)object;
            Licence licence = extractLicence(licenceKey);
            if (licence.addNewSession(licenceKey.getLogins().size(), user.getName())) {
                Login login = new Login(user, licenceKey, remoteHost);
                em.persist(login);
                em.merge(user);
                return login;
            }
        }
        log.warn("Failed to assign a licence for a key");
        Login login = new Login(user, null, remoteHost);
        em.persist(login);
        em.merge(user);
        return login;
        //return null;
    }

    /**
     * Try to release the licence from the specified login session.
     *
     * @param login the login session
     **/
    public void releaseLicence(Login login) {
        log.info(String.format("Releasing licence for user %s", login.getUser().getName()));
        login.setLicenceKey(null);
        em.merge(login);
    }
    
    // Add business logic below. (Right-click in editor and choose
    // "Insert Code > Add Business Method" or "Web Service > Add Operation")

    private Licence extractLicence(EncodedLicence licenceKey) {
        try {
            SignedLicenceKey so = readSignedLicenceKey(licenceKey.getEncodedLicence());
            ServerConfiguration serverConfig = ServerConfiguration.getInstance();
            PublicKey verificationKey = serverConfig.getLicenceVerificationKey();
            Signature verificationEngine = Signature.getInstance(verificationKey.getAlgorithm());
            if (true || so.verify(verificationKey, verificationEngine)) {
                Licence licence = so.getLicence();
                if (licence.isMatchingGUID(serverConfig.getClientGUID())) {
                    return licence;
                } else {
                    return null;
                }
            }
        } catch (Exception ex) {
            log.fatal(ex);
        }
        return null;
    }

    /**
     * Add a new licence key. Note that you don't have to be logged in to perform
     * this action!
     *
     * @param encodedLicence an encoded licence key
     * @return true to indicate success, false indicates the licence already exists
     **/
    public boolean addEncodedLicence(String encodedLicence) {
        // first strip any whitespace out of the string
        encodedLicence = encodedLicence.replaceAll("\\s", "");
        // test if it is a valid encoded licence
        try {
            SignedLicenceKey so = readSignedLicenceKey(encodedLicence);
        } catch(Exception e) {
            return false;
        }
        try {
            em.createNamedQuery("findByEncodedLicence")
              .setParameter("encodedLicence", encodedLicence)
              .getSingleResult();
            // this succeeded so the licence already exists!
            return false;
        } catch(NoResultException e) {
            EncodedLicence licenceKey = new EncodedLicence(encodedLicence);
            em.persist(licenceKey);
            return true;
        }
    }

    private SignedLicenceKey readSignedLicenceKey(String encodedLicence) throws IOException {
        byte[] compressedData = new BASE64Decoder().decodeBuffer(encodedLicence);
        Inflater decompressor = new Inflater();
        decompressor.setInput(compressedData);
        ByteArrayOutputStream bos = new ByteArrayOutputStream(compressedData.length);
        byte[] buf = new byte[1024];
        while (!decompressor.finished()) {
            try {
                int count = decompressor.inflate(buf);
                bos.write(buf, 0, count);
                if (count == 0) {
                    break;
                }
            } catch (DataFormatException e) {
            }
        }
        try {
            bos.close();
        } catch (IOException e) {
        }
        byte[] xmlData = bos.toByteArray();
        ByteArrayInputStream bis = new ByteArrayInputStream(xmlData);
        XMLDecoder in = new XMLDecoder(bis);
        return (SignedLicenceKey)in.readObject();
    }

    public void closeCurrentSessions() {
        log.info("Close current login sessions");

        List<Login> allLoggedOn = (List<Login>)em.createNamedQuery("findAllLogins").getResultList();
        Date logoutTime = new Date();
        for (Login login: allLoggedOn) {
            login.setLicenceKey(null);
            if (login.isLoggedOn()) {
                login.setLogoutTime(logoutTime);
                login.setLoginStatus(LoginStatus.RESET);
            }
        }
    }

    public void closeExpiredSessions() {
        try {
            // check for expired sessions
            Date date = new Date();
            List<Login> logins = (List<Login>)em.createNamedQuery("findAllLogins").getResultList();
            for (Login login: logins) {
                if (date.getTime() > login.getLastActionTime().getTime() + LOGOUT_DELAY) {
                    login.setLogoutTime(date);
                    login.setLoginStatus(LoginStatus.TIMED_OUT);
                }
            }
        } catch(Exception e) {
            log.error("Exception in performTimerTask!", e);
        }
    }

    public Collection<Licence> findAllLicences(KeyLocal key) {
        key.validateKey();
        Collection<Licence> allLicences = new ArrayList<Licence>();
        for (Object object: em.createNamedQuery("findAllLicenceKeys").getResultList()) {
            EncodedLicence licenceKey = (EncodedLicence)object;
            allLicences.add(extractLicence(licenceKey));
        }
        return allLicences;
    }

}
